import datetime
import logging
import math
import re

from itertools import groupby
from ast import literal_eval
from collections import defaultdict
from dateutil.relativedelta import relativedelta

from odoo import api, fields, models, _, Command, SUPERUSER_ID
from odoo.addons.web.controllers.utils import clean_action
from odoo.exceptions import UserError, ValidationError, AccessError
from odoo.tools import float_compare, float_round, float_is_zero, format_datetime
from odoo.tools.misc import OrderedSet, format_date, groupby as tools_groupby


class MrpProductionExt(models.Model):
    """ Manufacturing Orders """
    _inherit = 'mrp.production'

    qty_reject = fields.Float('Process Scrap', digits='Product Unit of Measure', compute='compute_qty_reject', store=True)
    s_order_type = fields.Selection([
        ('N', 'NEW'),
        ('R', 'REPEAT'),
        ('C', 'CHANGE'),
    ], 'Associate order type', readonly=True)
    mo_type = fields.Selection([
        ('N', 'Normal'),
        ('R', 'Rework'),
        ('M', 'Remake'),
        ('W', 'Overhaul'),
        ('C', 'Convert')
    ], string='Type', default='N', required=True)
    srce_rm_mo = fields.Char(string='Rework/Remake Source Manufacturing Order')

    sb_child_cg_ids = fields.Many2many(
        'mrp.production', 'sb_production_cg_rel', 'mo1_id', 'mo2_id', string='Rework', compute="_compute_sb_childs")
    sb_child_cz_ids = fields.Many2many(
        'mrp.production', 'sb_production_cz_rel', 'mo1_id', 'mo2_id', string='Remake', compute="_compute_sb_childs")
    sb_child_cg_qty = fields.Integer(string='Rework Quantity', compute="_compute_sb_childs")
    sb_child_cz_qty = fields.Integer(string='Remake Quantity', compute="_compute_sb_childs")

    uom_value_ids = fields.One2many('mrp.production.uom.value', 'production_id', 'Production Uom Value', copy=True)
    param_line_ids = fields.One2many('mrp.production.param.line', 'production_id', 'Production Params', copy=True)
    spec_line_ids = fields.One2many('mrp.production.spec.line', 'production_id', 'Production Specifications', copy=True)

    s_value_pnl = fields.Integer('PCS/PNL', compute='_compute_unit_value', store=True)
    s_value_set = fields.Integer('PCS/SET', compute='_compute_unit_value', store=True)

    is_autocomplete = fields.Boolean(string='autocomplete', default=False, copy=False, readonly=True)
    # customer = fields.Many2one('res.partner', 'Customer', compute="_compute_customer", stroe=True, readonly=False)
    customer = fields.Many2one('res.partner', 'Customer', readonly=False)
    pn = fields.Char(string='PN', compute="_compute_pn")
    special_order_type = fields.Selection([
        ('null', 'Null'),
        ('eq_only', 'EQ only'),
        ('free_cost', 'Free cost'),
        ('in_stock', 'In stock'),
        ('wb', 'WB'),
        ('prepare_materials', 'Prepare Materials'),
    ], string="Special Order Type", compute="_compute_special_order_type")
    is_automatic_consumption = fields.Boolean(default=False, compute="_compute_is_automatic_consumption", store=True)
    # components_availability_state
    date_code = fields.Char(string="DATE CODE", copy=False, readonly=True)
    state_priority = fields.Integer(compute='_compute_state_priority', store=True)  # 辅助状态排序

    impedance_ids = fields.Many2many(string='Impedance', related='product_tmpl_id.impedance_ids')
    stack_up_image = fields.Image(string='stack-up', related='product_tmpl_id.stack_up_image')
    # drill_image = fields.Image(string='Drill', compute='_compute_drill_image')

    s_order_product_qty = fields.Float(string='order product qty', digits='Product Unit of Measure', readonly=True, copy=False)
    panel_uom_qty = fields.Float(string='PANEL Quantity of feeding ', compute='_compute_panel_qty', store=True, readonly=True, copy=False)
    panel_qty_producing = fields.Float(string='Currently in production PANEL qty', compute='_compute_panel_qty', store=True, readonly=True, copy=False)
    panel_qty = fields.Float(string='PANEL Quantity', compute='_compute_panel_qty', store=True, readonly=True, copy=False)

    s_material_picking_ids = fields.Many2many(
        'stock.picking', 's_mrp_production_stock_picking_rel', string='material picking', copy=True,
    )
    s_order_suspension = fields.Boolean(string='Order suspension', default=False, copy=False, readonly=True)

    s_sale_order_line_ids = fields.Many2many('sale.order.line', 'mrp_production_sale_order_line_rel', copy=False)
    s_development_line_ids = fields.Many2many('s.research.and.development.order.line', 'mrp_production_s_research_and_development_order_line_rel', copy=False)

    s_makeup_order = fields.Boolean('Makeup Order', default=False, copy=False, readonly=True)  # A/B拼版订单标记字段
    s_makeup_product_id = fields.Many2one('product.product', 'Makeup Product', copy=False, readonly=True)  # A/B拼版的原产品，在订单完成后需要转换为此产品 ，已废弃
    s_done_convert = fields.Boolean('Done Convert', default=False, copy=False, readonly=True)

    active = fields.Boolean('Active', default=True, copy=False, readonly=True)

    s_not_scrap_verification = fields.Boolean('not scrap verification', copy=False, readonly=True)  # 压合打报废需要校验字段（辅助字段，有的生产单客户不需要校验）

    s_registered = fields.Boolean('Registered', copy=False, readonly=True, default=False)  # 报废是否登记字段
    s_processed_scrap = fields.Boolean('Processed scrap', copy=False, readonly=True)
    s_scrap_line_ids = fields.One2many('s.workorder.scrap.record', 'production_id')
    s_scrap_line_count = fields.Integer('Scrap Details Count', compute='compute_scrap_line_count', store=True)

    s_printed = fields.Boolean(string='Printed', default=False, copy=False)

    s_origin_delivery_date = fields.Datetime('Origin delivery date', compute='compute_s_origin_delivery_date', store=True)

    s_priority = fields.Integer('Priority', copy=False, readonly=True, default=10)

    s_station_order_ids = fields.One2many('s.mrp.work.station.order', 'production_id', 'Station Order')

    s_feeding_date = fields.Datetime('Feeding date', copy=False, readonly=True, default=fields.Datetime.now)

    s_base_mo_id = fields.Many2one('s.mrp.base', string='Base Mo')

    s_receipt_qty = fields.Integer('receipt quantity', copy=False, readonly=True)  # 成品入库数量
    s_pre_production_qty = fields.Integer('pre-production quantity', copy=False, readonly=True)  # 生产前成品数量

    s_child_production_ids = fields.Many2many('mrp.production', 's_mrp_production_production', 'source_id', 'child_id')
    s_parent_production_ids = fields.Many2many('mrp.production', 's_mrp_production_production', 'child_id', 'source_id')
    s_confirmed = fields.Boolean('Confirmed', copy=False, default=False, readonly=True)  # 已确认半成品生产制造单标记字段

    # ----------------名称计算字段--------------------
    # PR-XX-000150-01-101
    # s_first - s_second - base_mo - seq_no - level_no + s_last_seq_no
    s_first = fields.Char('first paragraph')
    s_second = fields.Char('Second paragraph', default='XX')
    base_mo = fields.Char(string='Baseline Manufacturing Order', index=True)
    level_no = fields.Integer(string='Level')
    seq_no = fields.Integer(string='Sequence Number', index=True)
    s_last_seq_no = fields.Integer(string='last Sequence Number', default=1, index=True, copy=False)

    # -------------------------------------------------

    @api.depends('s_sale_order_line_ids.order_id.planned_receiving_date', 's_development_line_ids.order_id.planned_receiving_date')
    def compute_s_origin_delivery_date(self):
        for rec in self.sorted(lambda x: x.id):
            delivery_date = False
            if rec.s_sale_order_line_ids:
                sale_lines = rec.s_sale_order_line_ids.filtered(lambda x: x.s_planned_receiving_date)
                if sale_lines:
                    delivery_date = min(sale_lines.mapped('s_planned_receiving_date'))
            elif rec.s_development_line_ids:
                development_line_ids = rec.s_development_line_ids.filtered(lambda x: x.order_id.planned_receiving_date)
                if development_line_ids:
                    delivery_date = min(development_line_ids.mapped('order_id.planned_receiving_date'))
            else:
                sources = rec._get_all_sources()
                sources -= rec
                date_list = sources.filtered(lambda x: x.s_origin_delivery_date).mapped('s_origin_delivery_date')
                if date_list:
                    delivery_date = min(date_list)

            rec.s_origin_delivery_date = delivery_date

    @api.depends('uom_value_ids.uom_value')
    def _compute_unit_value(self):
        for record in self:
            uom_values = {x.uom_id.code: x.uom_value for x in record.uom_value_ids}
            value_pnl = uom_values.get('PANEL', 1)
            value_set = uom_values.get('SET', 1)
            record.update({
                's_value_pnl': value_pnl,
                's_value_set': value_set
            })

    @api.depends('s_scrap_line_ids')
    def compute_scrap_line_count(self):
        for rec in self:
            rec.s_scrap_line_count = len(rec.s_scrap_line_ids)

    @api.depends('procurement_group_id', 'procurement_group_id.stock_move_ids.group_id', 's_material_picking_ids')
    def _compute_picking_ids(self):
        super()._compute_picking_ids()
        for rec in self:
            rec.picking_ids |= rec.s_material_picking_ids
            rec.delivery_count = len(rec.picking_ids)

    @api.depends('workorder_ids.qty_op_reject')
    def compute_qty_reject(self):
        for rec in self:
            rec.qty_reject = sum(rec.workorder_ids.mapped('qty_op_reject'))
            # if rec.qty_reject > 0:
            #     rec.with_delay().scrap_warning()

    def scrap_warning(self):
        for rec in self:
            production_ids = self.search([
                ('product_id', '=', rec.product_id.id),
                ('base_mo', '=', rec.base_mo)
            ])
            total = sum(production_ids.mapped('product_uom_qty')) or 1
            ratio = rec.qty_reject / total
            if ratio > 0.05:
                self.env['mail.message'].post_message_to_group_user(
                    'pcbmanufact.group_s_scrap_message_manager',
                    _('Product: %s , Production: %s, Currently scrapped %.2g %%', rec.product_id.name, rec.base_mo, ratio * 100)
                )

    @api.constrains('base_mo')
    def constrains_base_mo(self):
        for rec in self:
            if rec.s_base_mo_id and rec.s_base_mo_id.product_id:
                continue
            if not rec.base_mo:
                continue
            s_base_mo_id = self.env['s.mrp.base'].search([('name', '=', rec.base_mo)], limit=1)
            if not s_base_mo_id:
                s_base_mo_id = self.env['s.mrp.base'].sudo().create({'name': rec.base_mo})

            if not s_base_mo_id.product_id:
                if rec.product_id.categ_id.code and rec.product_id.categ_id.code == 'CHP':
                    s_base_mo_id.product_id = rec.product_id.id

            rec.s_base_mo_id = s_base_mo_id.id

    @api.constrains('state')
    def constrains_state_one(self):
        for rec in self:
            if rec.state != 'done':
                continue

            # 最上层生产单自动报废
            rec.finished_product_scrap()
            # 完成成品转换 A/B板
            # rec.finished_product_conversion()
            rec.lot_producing_id.production_date = datetime.datetime.now()
            # 源生产订单触发计算，有时下级工单完成没触发导致WIP为0
            rec._sources_compute_wip()

    def _sources_compute_wip(self):
        for rec in self:
            sources = rec._get_sources()
            for production in sources:
                workorder_ids = production.workorder_ids
                if workorder_ids:
                    workorder_ids = workorder_ids.sorted(key='step')
                    workorder_ids[0].compute_qty_floor()

    def create_production_product_lot(self):
        """
        创建成品批次
        """
        self.ensure_one()
        category_id = self.product_id.categ_id

        if category_id and category_id.code == 'CHP':
            production_cycle = f'-{self.date_code}' if self.date_code else ''
            lot_name = f'{self.product_id.name}-{self.base_mo}-{str(self.seq_no).zfill(2)}-{production_cycle}'
        else:
            lot_name = self.name
        lot_id = self.env['stock.lot'].search([('name', '=', lot_name), ('product_id', '=', self.product_id.id)], limit=1)
        if not lot_id:
            lot_id = self.env['stock.lot'].sudo().create({
                'name': lot_name,
                'product_id': self.product_id.id,
                's_production_id': self.id
            })
        return lot_id

    def finished_product_scrap(self):
        """
        根据报废明细，自动完成报废
        """
        self.ensure_one()
        if self._get_sources() or self.s_processed_scrap:
            return

        scrap_qty = 0
        scrap_vals_list = []
        for line in self.workorder_ids.mapped('s_scrap_ids'):
            if float_is_zero(line.scrap_qty, precision_rounding=1):
                continue
            scrap_qty += line.scrap_qty
            scrap_vals_list.append((0, 0, {
                'production_id': self.id,
                'rep_workorder_id': line.scrap_workorder_id.id,
                'scrapreason_id': line.scrap_reason_id.id,
                'qty_reject': line.scrap_qty,
            }))
        if scrap_vals_list or not float_is_zero(scrap_qty, precision_rounding=1):
            location_id = self.env['stock.location'].sudo().search([('barcode', '=', 'WH-PREPRODUCTION')], limit=1)
            vals = {
                'scrap_type': 'prod',
                'product_id': self.product_id.id,
                'product_uom_id': self.product_uom_id.id,
                'location_id': location_id.id,
                'production_id': self.id,
                'lot_id': self.lot_producing_id.id,
                'scrap_qty': scrap_qty,
                'origin': self.name,
                'scrap_line_ids': scrap_vals_list
            }
            scrap_id = self.env['stock.scrap'].with_user(SUPERUSER_ID).create(vals)
            scrap_id.action_validate()
        self.s_processed_scrap = True

    def action_cancel(self):
        if not any([self.env.is_superuser(), self.env.user.has_group('pcbmanufact.group_production_cancel_manager')]):
            raise AccessError(_('You do not have permission to cancel the production order!'))
        return super().action_cancel()

    def get_lot_id(self, lot_name, product_id):
        lot_id = self.env['stock.lot'].search([
            ('name', '=', lot_name),
            ('product_id', '=', product_id.id)
        ], limit=1)
        if not lot_id:
            lot_id = self.env['stock.lot'].create({
                'name': lot_name,
                'product_id': product_id.id
            })
        return lot_id

    # def finished_product_conversion(self):
    #     """
    #     完成成品转换 A/B板
    #     """
    #     self.ensure_one()
    #     if self.state != 'done':
    #         return
    #     if not all([self.s_makeup_order, self.s_makeup_product_id, not self.s_done_convert]):
    #         return
    #
    #     if self.s_makeup_product_id == self.product_id:
    #         return
    #     convert_qty = self.qty_produced - self.qty_reject
    #     rounding = self.product_id.uom_id.rounding
    #     if float_is_zero(convert_qty, precision_rounding=rounding):
    #         return
    #
    #     picking_type = self.env['stock.picking.type'].search([('code', '=', 'mrp_operation')], limit=1)
    #     location_id = self.env['stock.location'].search([('usage', '=', 'production'), ('company_id', '=', self.env.user.company_id.id)], limit=1)
    #     lot_id = self.get_lot_id(self.lot_producing_id.name, self.s_makeup_product_id)
    #
    #     move_finished_id = self.move_finished_ids[0]
    #     convert_production = self.sudo().create({
    #         'company_id': self.env.user.company_id.id,
    #         'product_id': self.s_makeup_product_id.id,
    #         'product_uom_id': self.product_uom_id.id,
    #         'lot_producing_id': lot_id.id,
    #         'bom_id': None,
    #         'active': False,
    #         'origin': f"{self.name or ''} Convert",
    #         'consumption': 'strict',
    #         'date_start': datetime.datetime.now(),
    #         'mo_type': 'C',
    #         'product_qty': convert_qty,
    #         'qty_producing': convert_qty,
    #         'picking_type_id': picking_type.id,
    #         'location_dest_id': move_finished_id.location_dest_id.id,
    #         'location_src_id': move_finished_id.location_dest_id.id,
    #         's_sale_order_line_ids': [(4, s.id) for s in self.s_sale_order_line_ids],
    #         's_development_line_ids': [(4, s.id) for s in self.s_development_line_ids],
    #         'move_finished_ids': [(0, 0, {
    #             'company_id': self.env.user.company_id.id,
    #             'date': datetime.datetime.now(),
    #             'location_dest_id': move_finished_id.location_dest_id.id,
    #             'location_id': location_id.id,
    #             'name': self.s_makeup_product_id.display_name,
    #             'procure_method': 'make_to_stock',
    #             'product_id': self.s_makeup_product_id.id,
    #             'product_uom': self.product_uom_id.id,
    #             'product_uom_qty': convert_qty,
    #             'unit_factor': 1,
    #             'price_unit': move_finished_id.price_unit
    #         })],
    #         'move_raw_ids': [(0, 0, {
    #             'company_id': self.env.user.company_id.id,
    #             'date': datetime.datetime.now(),
    #             'location_dest_id': location_id.id,
    #             'location_id': move_finished_id.location_dest_id.id,
    #             'name': self.product_id.display_name,
    #             'procure_method': 'make_to_stock',
    #             'product_id': self.product_id.id,
    #             'product_uom': self.product_uom_id.id,
    #             'product_uom_qty': convert_qty,
    #             'unit_factor': 1,
    #             'price_unit': move_finished_id.price_unit
    #         })],
    #     })
    #     convert_production = convert_production.sudo()
    #     convert_production.action_confirm()
    #
    #     finished_lot_vals = {}
    #     move_line_ids = self.finished_move_line_ids + self.scrap_ids.mapped('move_ids.move_line_ids')
    #     for line in move_line_ids:
    #         if line.location_id.usage == 'internal':
    #             qty = -line.quantity
    #         else:
    #             qty = line.quantity
    #         if line.lot_id in finished_lot_vals:
    #             finished_lot_vals[line.lot_id] += qty
    #         else:
    #             finished_lot_vals[line.lot_id] = qty
    #
    #     # convert_production.move_raw_ids._action_done()
    #     convert_production.move_raw_ids.mapped('move_line_ids').unlink()
    #     raw_move_ = convert_production.move_raw_ids[0]
    #     convert_production.move_finished_ids.mapped('move_line_ids').unlink()
    #     finished_move_ = convert_production.move_finished_ids[0]
    #     move_line_vals_list = []
    #     for lot, qty in finished_lot_vals.items():
    #         raw_move_vals = raw_move_._prepare_move_line_vals(qty)
    #         raw_move_vals.update({
    #             'lot_id': lot.id,
    #             'picked': True
    #         })
    #         move_line_vals_list.append(raw_move_vals)
    #
    #         finished_lot_id = self.get_lot_id(lot.name, self.s_makeup_product_id)
    #         if finished_lot_id == convert_production.lot_producing_id:
    #             continue
    #         finished_move_vals = finished_move_._prepare_move_line_vals(qty)
    #         finished_move_vals.update({
    #             'lot_id': finished_lot_id.id,
    #             'picked': True
    #         })
    #         move_line_vals_list.append(finished_move_vals)
    #     self.env['stock.move.line'].create(move_line_vals_list)
    #     raw_move_._action_done()
    #     finished_move_._action_done()
    #     convert_production.button_mark_done()
    #
    #     self.s_done_convert = True

    @api.depends('product_uom_qty', 'qty_producing', 'product_qty', 'bom_id')
    def _compute_panel_qty(self):
        for rec in self:
            rec.panel_uom_qty = rec.product_uom_qty * rec.bom_id.pcs_panel_ratio
            rec.panel_qty_producing = rec.qty_producing * rec.bom_id.pcs_panel_ratio
            rec.panel_qty = rec.product_qty * rec.bom_id.pcs_panel_ratio

    @api.depends('company_id', 'bom_id', 'product_id', 'product_qty', 'product_uom_id', 'location_src_id', 'date_start')
    def _compute_move_raw_ids(self):
        super()._compute_move_raw_ids()
        for rec in self:
            ratio = rec.bom_id.pcs_panel_ratio
            for move in rec.move_raw_ids:
                move.panel_need_qty = move.product_uom_qty * ratio
                # if move.state in ('done', 'cancel'):
                #     continue

    @api.constrains('product_id', 'product_qty')
    def _compute_move_raw_ids_cr(self):
        """
        计算板料的用量，A/B板不在此方法计算（投料分摊）
        """
        for rec in self:
            if rec.state in ('done', 'cancel'):
                continue
            if rec.s_makeup_order:
                continue
            for move in rec.move_raw_ids:
                if move.state in ('done', 'cancel'):
                    continue
                if move.product_id._check_product_is_cr():
                    if rec.uom_value_ids:
                        uom_values = {x.uom_id.code: x.uom_value for x in rec.uom_value_ids}
                    else:
                        uom_values = {x.uom_id.code: x.uom_value for x in rec.product_id.uom_value_ids}
                    value_pnl = uom_values.get('PANEL', 1)
                    value_sheet = uom_values.get('SHEET', 1)
                    move.product_uom_qty = rec.product_qty / value_pnl / value_sheet

    def _compute_drill_image(self):
        for rec in self:
            if rec.workcenter_id:
                rec.drill_image = rec.workcenter_id.drill_image
            else:
                rec.drill_image = None

    # def _compute_drill_image(self):
    #     for rec in self:
    #         if rec.workcenter_id:
    #             rec.drill_image = rec.workcenter_id.drill_image
    #         else:
    #             rec.drill_image = None
    # def action_cancel(self):
    #     result = super().action_cancel()
    #     for rec in self:
    #         rec.s_sale_order_line_ids.mapped('s_mrp_production_ids')

    def _get_all_sources(self):

        def _get_all_sources(parent):
            sources = self.env['mrp.production']
            for p in parent:
                sources |= parent
                sources |= _get_all_sources(p._get_sources())
            return sources

        self.ensure_one()
        all_sources = self._get_sources()
        all_sources |= _get_all_sources(all_sources)
        return all_sources

    def _get_all_child(self):

        def _get_all_descendants(children):
            all_descendants = self.env['mrp.production']
            for child in children:
                all_descendants |= child
                all_descendants |= _get_all_descendants(child._get_children())
            return all_descendants

        self.ensure_one()
        all_child_ids = self._get_children()
        all_child_ids |= _get_all_descendants(all_child_ids)
        return all_child_ids

    def _get_children(self):
        production_ids = super()._get_children()
        production_ids |= self.s_child_production_ids
        return production_ids

    def _get_sources(self):
        production_ids = super()._get_sources()
        production_ids |= self.s_parent_production_ids
        return production_ids

    def _create_bcp_production(self):
        self.ensure_one()
        if self.s_confirmed:
            return
        location_dest_id = self.env['stock.location'].search([('barcode', '=', 'WH-PREPRODUCTION')], limit=1)
        if not location_dest_id:
            raise ValidationError(_('Please contact the administrator, feeding failed, missing pre production position'))
        vals_list = []
        for move in self.move_raw_ids:
            if not move.product_id.categ_id.code:
                continue
            if move.product_id.categ_id.code != 'BCP':
                continue
            if not move.product_id.bom_ids:
                continue
            vals_list.append({
                'consumption': 'strict',
                'date_start': datetime.datetime.now(),
                # 'mo_type': self.mo_type,
                'product_qty': self.product_qty,
                'picking_type_id': self.picking_type_id.id,
                'location_dest_id': location_dest_id.id,
                'location_src_id': location_dest_id.id,
                'base_mo': self.base_mo,
                'product_id': move.product_id.id,
                'bom_id': move.product_id.bom_ids[0].id,
                'origin': self.name,
                's_parent_production_ids': [(4, self.id)]
            })
        if vals_list:
            children_production_ids = self.create(vals_list)
            # children_production_ids.write({'location_dest_id': location_dest_id.id})
            children_production_ids.action_confirm()

    def action_confirm(self):
        """
        制造单确认后根据消耗物料，生成半成品工单
        """
        result = super().action_confirm()
        for rec in self:
            rec._create_bcp_production()
        self.write({'s_confirmed': True})
        self.compute_station_order(all_production=False)
        self.env['s.mrp.task'].with_user(SUPERUSER_ID).with_delay(priority=10).production_scheduling(
            self.filtered(lambda x: x.state not in ('cancel', 'draft', 'done')))
        self.mapped('workorder_ids').write({'state': 'pending'})
        return result

    # def action_confirm(self):
    #     """
    #     板材的用量是整张的， 按BOM标准用量计算是小数，与实际发料有差异，差异的部分直接加到最后一个同产品的库存移动中
    #     """
    #     result = super().action_confirm()
        # for rec in self:
            # rec.s_sale_order_line_ids.mapped('s_mrp_production_ids').filtered(lambda p: p.state == 'draft').action_confirm()
            # rec.s_development_line_ids.mapped('mrp_production_ids').filtered(lambda p: p.state == 'draft').action_confirm()
            # 需要所有关联生产单全部确认才可分摊  10-19 取消分摊，由_compute_move_raw_ids方法计算芯板用量
            # if rec.s_sale_order_line_ids.mapped('s_mrp_production_ids').filtered(lambda p: p.state == 'draft'):
            #     continue
            #
            # if rec.s_development_line_ids.mapped('mrp_production_ids').filtered(lambda p: p.state == 'draft'):
            #     continue
            #
            # material_picking_ids = rec.s_material_picking_ids.filtered(lambda p: p.s_material_order and not p.s_material_share)
            # if not material_picking_ids:
            #     continue
            # material_picking_ids.filtered(lambda p: p.state == 'draft').action_confirm()
            # material_usage = {}
            # for picking in material_picking_ids:
            #     for move in picking.move_ids:
            #         material_usage[move.product_id] = move.product_uom_qty
            #
            # all_production = self.env['mrp.production']
            # all_top_production = rec + rec.s_sale_order_line_ids.mapped('s_mrp_production_ids') + rec.s_development_line_ids.mapped('mrp_production_ids')
            # all_top_production = all_top_production.filtered(lambda p: p.state not in ('draft', 'done', 'cancel'))
            # for p in all_top_production:
            #     all_production |= p._get_all_child()
            # all_production |= all_top_production
            # # 绑定子工单和材料调拨单
            # all_production.write({'s_material_picking_ids': [(4, p.id) for p in material_picking_ids]})
            # all_move_raw_ids = all_production.mapped('move_raw_ids').filtered(lambda x: x.state not in ('draft', 'done', 'cancel'))
            # # 先取消保留
            # all_move_raw_ids._do_unreserve()
            # for product_id, g in groupby(all_move_raw_ids.sorted(key=lambda x: x.product_id.id), key=lambda x: x.product_id):
            #     product_move_ids = self.env['stock.move'].concat(*g)
            #     if product_id not in material_usage:
            #         continue
            #
            #     total = material_usage[product_id]
            #     move_total = sum(product_move_ids.mapped('product_uom_qty'))
            #     rounding = product_move_ids[0].product_uom.rounding
            #     contrast = float_compare(total, move_total, precision_rounding=rounding)
            #     if contrast != 0:
            #         for move in product_move_ids:
            #             move.product_uom_qty = float_round(total * (move.product_uom_qty / move_total), precision_rounding=move.product_uom.rounding)
            #         difference = total - sum(product_move_ids.mapped('product_uom_qty'))
            #         # 分配后的有小数差异，将差值放到最后一个生产单上
            #         if not float_is_zero(difference, precision_rounding=rounding):
            #             product_move_ids[-1].product_uom_qty += difference
            #
            # material_picking_ids.write({'s_material_share': True})
        # if self.filtered(lambda x: x.s_order_suspension):
        #     raise UserError('The order has been suspended and confirmation is not allowed!')
        # self.compute_station_order(all_production=False)
        # self.env['s.mrp.task'].with_user(SUPERUSER_ID).with_delay(priority=10).production_scheduling(self.filtered(lambda x: x.state not in ('cancel', 'draft', 'done')))
        # self.mapped('workorder_ids').write({'state': 'pending'})
        # return result

    @api.depends('state')
    def _compute_state_priority(self):
        for record in self:
            if record.state == 'progress':
                record.state_priority = 1
            elif record.state == 'comfirmed':
                record.state_priority = 2
            elif record.state == 'to_close':
                record.state_priority = 3
            elif record.state == 'done':
                record.state_priority = 4
            else:
                record.state_priority = 5  # 其他状态

    # 当组件可用后自动勾选已消耗
    @api.depends('components_availability_state')
    def _compute_is_automatic_consumption(self):
        for rec in self:
            if rec.components_availability_state == 'available':
                # mrp_production_ids = production.sudo()._get_sources()
                for move in rec.move_raw_ids:
                    move.picked = True
                    # if not move.picked and move.product_uom_qty == move.quantity:
                    #     move.picked = True

    @api.depends('origin')
    def _compute_special_order_type(self):
        for production in self:
            sot = production.env['sale.order'].search([('name', '=', production.origin)], limit=1).special_order_type
            production.special_order_type = sot if sot else None

    @api.depends('customer', 'product_id')
    def _compute_pn(self):
        for production in self:
            if production.customer:
                production.pn = production.env['customer.item.code'].search([
                    ('product_id', '=', production.product_id.product_tmpl_id.id),
                    ('customer_id', '=', production.customer.id)
                ], limit=1).code
            else:
                production.pn = None

    def _compute_is_autocomplete(self):
        for production in self:
            if all(record.state == 'done' for record in production.workorder_ids):
                if not production.is_autocomplete and production.state == 'to_close' and production.mrp_production_source_count > 0:
                    for move in production.move_raw_ids:
                        if not move.picked:
                            raise ValidationError(
                                'MO:' + production.name + _(" The component is abnormal and automatic production fails. "
                                                            "Please manually complete the MO"))

                    # skip_consumption  跳过物料消耗检验
                    production.with_context(skip_consumption=True).sudo().button_mark_done()
                    production.is_autocomplete = True

    def button_mark_done(self):
        self.with_context(bypass_duration_calculation=True, skip_expired=True)
        for p in self:
            if not p.lot_producing_id:
                lot_id = p.create_production_product_lot()
                p.lot_producing_id = lot_id.id
            for move in p.move_raw_ids.filtered(lambda x: x.state not in ('done', 'cancel')):
                if float_compare(move.product_uom_qty, move.quantity, precision_rounding=move.product_uom.rounding) == 1:
                    move.quantity = float_round(move.product_uom_qty, precision_digits=move.product_uom.rounding)
            workorder_ids = self.env['mrp.workorder'].search([('production_id', '=', p.id)])
            wip = 0
            for wo in workorder_ids:
                wip += wo.qty_wip
            if wip == 0:
                date_finished = fields.Datetime.now()
                date_start = fields.Datetime.now() - datetime.timedelta(seconds=1)
                for nwo in workorder_ids.filtered(lambda x: x.state != 'done'):
                    vals = {
                        'qty_produced': nwo.qty_produced or nwo.qty_producing or nwo.qty_production,
                        'state': 'done',
                        'date_start': date_start,
                        'date_finished': date_finished,
                        'costs_hour': nwo.workcenter_id.costs_hour
                    }
                    nwo.with_context(bypass_duration_calculation=True, skip_expired=True).write(vals)

        return super(MrpProductionExt, self.with_context(bypass_duration_calculation=True, skip_expired=True)).button_mark_done()

    def _compute_sb_childs(self):
        for rec in self:
            cg_ids = self.env['mrp.production'].search([('mo_type', '=', 'R'), ('origin', '=', rec.name)])
            cz_ids = self.env['mrp.production'].search([('mo_type', '=', 'M'), ('origin', '=', rec.name)])
            rec.sb_child_cg_ids = cg_ids
            rec.sb_child_cz_ids = cz_ids
            rec.sb_child_cg_qty = len(cg_ids)
            rec.sb_child_cz_qty = len(cz_ids)

    def _get_rm_name(self, vals):
        s_first = vals.get('s_first', '')
        # s_second = vals.get('s_second', 1)
        base_mo = vals.get("base_mo")
        # seq_no = vals.get("seq_no", 0)
        level_no = vals.get("level_no", 0)
        s_last_seq_no = vals.get("s_last_seq_no", 0)

        mo_type = vals.get("mo_type", "M")
        rma_mo_type = vals.get("mo_type", None)
        rma_id = vals.get('rma_id', None)
        # 获取是第几次补料
        # data = self.read_group(
        #     domain=[('base_mo', '=', base_mo), ('mo_type', '=', 'M')],
        #     fields=['__count'],
        #     groupby=['s_second']
        # )
        record = self.search([
            ('base_mo', '=', base_mo),
            ('active', 'in', (True, False)),
            ('mo_type', '=', mo_type)
        ], order='s_second desc', limit=1)
        count = 1
        if record:
            number_part = int(record.s_second[1:]) if len(record.s_second) > 1 and record.s_second[1:].isdigit() else 1
            count = number_part + 1
        # for item in data:
        #     count = item.get('s_second_count') + 1
        #     break

        if mo_type == 'R':
            s_second = f'R{count}'
        elif mo_type == 'W':
            s_second = f'W{count}'
        else:
            s_second = f'M{count}'
        if rma_id and rma_mo_type == 'R':
            s_second = f'R{count}'
        # 获取同base_mo 最大的 seq_no
        record = self.search([('base_mo', '=', base_mo)], order='seq_no desc', limit=1)
        seq_no = record.seq_no + 1 if record else 1
        for num in range(seq_no, 100):
            new_name_candidate = f"{s_first}-{s_second}-{base_mo}-{str(num).zfill(2)}-{level_no}{str(s_last_seq_no).zfill(2)}"
            if not self.env['mrp.production'].search([('name', '=', new_name_candidate)], limit=1):
                new_name = new_name_candidate
                seq_no = num
                break

        vals.update({
            's_second': s_second,
            'seq_no': seq_no
        })
        return new_name

    def _get_inner_name(self, vals):
        origin = vals.get("origin")
        s_first = vals.get('s_first', '')
        s_second = vals.get('s_second', 'XX')
        base_mo = vals.get("base_mo")
        seq_no = vals.get("seq_no", 1)
        level_no = vals.get("level_no", 0)
        s_last_seq_no = vals.get("s_last_seq_no", 1)

        # 优化 level_no 的获取逻辑
        if origin:
            origin_order = self.env['mrp.production'].search(
                [('name', '=', origin)], limit=1, order='id desc'
            )
            if origin_order:
                s_first = origin_order.s_first
                seq_no = origin_order.seq_no
                s_second = origin_order.s_second
                level_no = origin_order.level_no + 1

        op = f"{s_first}-{s_second}-{base_mo}-{str(seq_no).zfill(2)}"

        # 生成唯一名称的循环优化
        new_name = ""
        for num in range(s_last_seq_no, 100):
            new_name_candidate = f"{op}-{level_no}{str(num).zfill(2)}"
            if not self.env['mrp.production'].search([('name', '=', new_name_candidate)], limit=1):
                new_name = new_name_candidate
                s_last_seq_no = num
                break

        vals.update({
            's_second': s_second,
            's_last_seq_no': s_last_seq_no,
            'level_no': level_no,
            'seq_no': seq_no,
            's_first': s_first
        })
        return new_name

    def _get_name(self, vals, seq_no=1):
        new_wo = ""
        origin = vals.get("origin")
        base_mo = vals.get("base_mo")
        mo_type = vals.get("mo_type", "N")
        level_no = vals.get("level_no", 0)
        if not seq_no:
            seq_no = vals.get("seq_no", seq_no)
        s_first = vals.get('s_first', '')
        s_second = vals.get('s_second', 'XX')

        if mo_type in ['R', 'M', 'W']:
            new_wo = self._get_rm_name(vals)
        else:
            if origin:
                latest_order = self.env['mrp.production'].search([('name', '=', origin)], limit=1, order='id desc')
                if latest_order:
                    vals.update({
                        'mo_type': latest_order.mo_type,
                        'base_mo': latest_order.base_mo,
                    })
                    new_wo = self._get_inner_name(vals)
                else:
                    for num in range(seq_no, 100):
                        new_name_candidate = f"{s_first}-{s_second}-{base_mo}-{str(num).zfill(2)}-000"
                        if not self.env['mrp.production'].search([('name', '=', new_name_candidate)], limit=1):
                            new_wo = new_name_candidate
                            seq_no = num
                            break
                    vals.update({
                        'level_no': level_no,
                        'seq_no': seq_no,
                        's_second': s_second,
                    })
            else:
                for num in range(seq_no, 100):
                    new_name_candidate = f"{s_first}-{s_second}-{base_mo}-{str(num).zfill(2)}-000"
                    if not self.env['mrp.production'].search([('name', '=', new_name_candidate)], limit=1):
                        new_wo = new_name_candidate
                        seq_no = num
                        break
                vals.update({
                    'level_no': level_no,
                    'seq_no': seq_no,
                    's_second': s_second,
                })
        return new_wo

    @api.model_create_multi
    def create(self, vals_list):
        """
        生成订单号，按 product_id 和 origin 分组生成 base_mo
        格式：QN-XX-000001-00-000
        Q: product_template.materiel_status
        N: sale_order.order_type
        XX:
        000001: base_wo
        000:
        """
        # 按 product_id 和 origin 排序（确保相同组合的记录连续）
        vals_list.sort(key=lambda x: (x.get('product_id'), x.get('origin', '')))

        name_list = []  # 用于存储已生成的名称，避免重复

        # 分组键：(product_id, origin)
        for (product_id, origin), g_vals in groupby(
                vals_list,
                key=lambda x: (x.get('product_id'), x.get('origin'))
        ):
            # 将分组后的记录转为列表，方便多次迭代
            grouped_vals = list(g_vals)
            product = self.env['product.product'].browse(product_id)
            if not product_id:
                raise ValidationError('Product required!')
            if not product.materiel_status:
                raise ValidationError(
                    _('Product (%s) materiel status is null, Please provide supplementary information for the project!',
                      product.name))
            seq_vals = {}
            # 如果 origin 有值，说明是同一批次，共享同一个 base_mo
            if origin:
                latest_order = self.env['mrp.production'].search([('name', '=', origin)], limit=1, order='id desc')
                if latest_order:
                    base_mo = latest_order.base_mo
                else:
                    base_mo = None

                for vals in grouped_vals:
                    vals_base_mo = vals.get('base_mo')
                    if not vals_base_mo:
                        if not base_mo:
                            base_mo = self.env['ir.sequence'].next_by_code('mrp.production.pcb')
                        vals_base_mo = base_mo

                    record = self.search([('base_mo', '=', vals_base_mo)], order='seq_no desc', limit=1)
                    seq_no = record.seq_no + 1 if record else 1
                    if vals_base_mo in seq_vals:
                        seq_no = seq_vals.get(vals_base_mo, seq_no)
                    else:
                        seq_vals[vals_base_mo] = seq_no
                    self._process_single_record(product, vals, vals_base_mo, name_list, seq_no)
                    seq_vals[vals_base_mo] += 1
            else:
                # 如果 origin 为空，每条记录单独生成 base_mo
                for vals in grouped_vals:
                    vals_base_mo = vals.get('base_mo') or self.env['ir.sequence'].next_by_code('mrp.production.pcb')
                    self._process_single_record(product, vals, vals_base_mo, name_list)

        # 调用父类的 create 方法
        return super().create(vals_list)

    def _process_single_record(self, product_id, vals, base_mo, name_list, seq_no=None):
        """
        处理单条记录，生成唯一名称并更新 vals
        """
        if vals.get('name', False):  # 如果已经有 name，跳过
            return

        # 设置 base_mo
        vals['base_mo'] = base_mo
        if 's_first' not in vals.keys():
            s_order_type = vals.get('s_order_type', 'N')
            vals['s_first'] = f"{product_id.materiel_status}{s_order_type or 'N'}"
        # 生成唯一名称
        name = self._generate_unique_name(vals, name_list, seq_no)
        name_list.append(name)
        vals['name'] = name

    def _generate_unique_name(self, vals, name_list, seq_no=None):
        """
        生成唯一的名称
        """

        name = self._get_name(vals, seq_no)  # 初始名称
        seq_int = vals.get('seq_no', 1)

        while name in name_list:  # 如果名称已存在，则递增序列号
            n_list = name.split('-')
            last = n_list[-1] or ''
            if last == '000':
                n_list[-2] = f"{seq_int:02d}"
                vals['seq_no'] = seq_int
                seq_int += 1
            else:
                level_no = vals.get("level_no", 1)
                s_last_seq_no = vals.get("s_last_seq_no", 1)
                s_last_seq_no += 1
                n_list[-1] = f"{level_no}{s_last_seq_no:02d}"  # 更新最后一个部分为三位数格式
                vals.update({
                    'level_no': level_no,
                    's_last_seq_no': s_last_seq_no
                })
            name = '-'.join(n_list)

        return name

    def compute_attribute(self):
        for production in self:
            if production.product_id and production.product_qty > 0:
                # 复制单位，规格，参数
                if not production.uom_value_ids:
                    uom_value_vals = []
                    for x in production.product_id.uom_value_ids:
                        uom_value = production.uom_value_ids.filtered(lambda u: u.uom_id == x.uom_id)
                        if uom_value:
                            uom_value.write({'uom_value': x.uom_value})

                        else:
                            uom_value_vals.append((0, 0, {
                                'uom_id': x.uom_id.id,
                                'uom_value': x.uom_value,
                            }))
                    if uom_value_vals:
                        production.uom_value_ids = uom_value_vals

                if not production.param_line_ids:
                    valu = []
                    for x in production.product_id.param_line_ids:
                        uom_value = production.param_line_ids.filtered(lambda u: u.attribute_id == x.attribute_id)
                        if uom_value:
                            uom_value.write({'value_ids': x.value_id.id})
                        else:
                            valu.append((0, 0, {
                                'attribute_id': x.attribute_id.id,
                                'value_id': x.value_id.id,
                            }))

                    if valu:
                        production.write({
                            'param_line_ids': valu
                        })

                if not production.spec_line_ids:
                    valu = []
                    for x in production.product_id.spec_line_ids:
                        uom_value = production.spec_line_ids.filtered(lambda u: u.attribute_id == x.attribute_id)
                        if uom_value:
                            uom_value.write({'value_ids': x.value_id.id})
                        else:
                            valu.append((0, 0, {
                                'attribute_id': x.attribute_id.id,
                                'value_id': x.value_id.id,
                            }))
                    if valu:
                        production.write({
                            'spec_line_ids': valu
                        })

                for workorder in production.workorder_ids:
                    if not workorder.operation_id:
                        continue
                    # workorder.sb_scgy = workorder.operation_id.sb_scgy
                    # workorder.sb_auto_complete = workorder.operation_id.sb_auto_complete
                    # workorder.is_subcontracting = workorder.workcenter_id.is_sub_contract

                    if not workorder.param_ids:
                        val_list = []
                        for x in workorder.operation_id.param_line_ids:
                            val_list.append((0, 0, {
                                'attribute_id': x.attribute_id.id,
                                'value_id': x.value_id.id,
                            }))
                        workorder.param_ids = val_list

                    # 钻孔数据
                    if not workorder.s_drill_operation_ids:
                        for rec in workorder.operation_id.drill_operation_ids:
                            workorder.s_drill_operation_ids = [(4, rec.id)]
                    # 背钻表
                    for rec in workorder.operation_id.back_drill_operation_ids:
                        workorder.s_back_drill_operation_ids = [(4, rec.id)]

    @api.depends('bom_id', 'product_id', 'product_qty', 'product_uom_id')
    def _compute_workorder_ids(self):
        """
        填充工艺的属性，自动过数,外包
        """
        res = super(MrpProductionExt, self)._compute_workorder_ids()
        for production in self:
            if production.state != 'draft':
                continue
            if production.product_id and production.product_qty > 0:
                # 复制单位，规格，参数
                if not production.uom_value_ids:
                    uom_value_vals = []
                    for x in production.product_id.uom_value_ids:
                        uom_value = production.uom_value_ids.filtered(lambda u: u.uom_id == x.uom_id)
                        if uom_value:
                            uom_value.write({'uom_value': x.uom_value})

                        else:
                            uom_value_vals.append((0, 0, {
                                'uom_id': x.uom_id.id,
                                'uom_value': x.uom_value,
                            }))
                    if uom_value_vals:
                        production.uom_value_ids = uom_value_vals

                if not production.param_line_ids:
                    valu = []
                    for x in production.product_id.param_line_ids:
                        uom_value = production.param_line_ids.filtered(lambda u: u.attribute_id == x.attribute_id)
                        if uom_value:
                            uom_value.write({'value_ids': x.value_id.id})
                        else:
                            valu.append((0, 0, {
                                'attribute_id': x.attribute_id.id,
                                'value_id': x.value_id.id,
                            }))

                    if valu:
                        production.write({
                            'param_line_ids': valu
                        })

                if not production.spec_line_ids:
                    valu = []
                    for x in production.product_id.spec_line_ids:
                        uom_value = production.spec_line_ids.filtered(lambda u: u.attribute_id == x.attribute_id)
                        if uom_value:
                            uom_value.write({'value_ids': x.value_id.id})
                        else:
                            valu.append((0, 0, {
                                'attribute_id': x.attribute_id.id,
                                'value_id': x.value_id.id,
                            }))
                    if valu:
                        production.write({
                            'spec_line_ids': valu
                        })

                for workorder in production.workorder_ids:
                    if not workorder.operation_id:
                        continue
                    # workorder.sb_scgy = workorder.operation_id.sb_scgy
                    # workorder.sb_auto_complete = workorder.operation_id.sb_auto_complete
                    # workorder.is_subcontracting = workorder.workcenter_id.is_sub_contract
                    val_list = []
                    for x in workorder.operation_id.param_line_ids:
                        val_list.append((0, 0, {
                            'attribute_id': x.attribute_id.id,
                            'value_id': x.value_id.id,
                        }))
                    workorder.param_ids = val_list

                    # 钻孔数据
                    for rec in workorder.operation_id.drill_operation_ids:
                        workorder.s_drill_operation_ids = [(4, rec.id)]
                    for rec in workorder.operation_id.back_drill_operation_ids:
                        workorder.s_back_drill_operation_ids = [(4, rec.id)]
        return res

    def unlink_plan(self):
        for mo in self:
            mrp_production_ids = mo._get_children()
            for c in mrp_production_ids:
                c.unlink_plan()
            mo.action_cancel()
            workorders_to_delete = mo.workorder_ids.filtered(lambda wo: wo.state != 'done')
            if workorders_to_delete:
                workorders_to_delete.unlink()
            res = super(MrpProductionExt, mo).unlink()

    # def check_semi_finished_products(self):
    #     """
    #     检查半成品保留的数量是否等于需求数
    #     """
    #     self.ensure_one()
    #     result = True
    #     for move in self.move_raw_ids:
    #         if not move.product_id.categ_id.code:
    #             continue
    #         if move.product_id.categ_id.code != 'BCP':
    #             continue
    #         if float_compare(move.product_uom_qty, move.quantity, precision_rounding=move.product_uom.rounding) != 0:
    #             return False
    #     return result

    def consumable_materials(self):
        self.ensure_one()

        move_raw_ids = self.move_raw_ids.filtered(lambda x: x.state not in ('done', 'cancel'))
        product_lots = defaultdict(dict)
        for move in move_raw_ids:
            if move.product_id.tracking == 'none':
                continue  # 非批次追踪产品跳过
            # 根据move_orig_ids决定库存范围
            domain = [
                ('product_id', '=', move.product_id.id),
                ('location_id', '=', move.location_id.id),
            ]
            if move.move_orig_ids:
                orig_lot_ids = move.move_orig_ids.mapped('lot_ids').ids
                domain.append(('lot_id', 'in', orig_lot_ids))
            else:
                domain.append(('lot_id', '!=', False))  # 确保有批次号

            quants = self.env['stock.quant'].search(domain)
            product_lots[move.product_id.id] = quants

        # 处理每个分类下的移动
        for move in move_raw_ids:
            product = move.product_id
            rounding = product.uom_id.rounding
            move_qty = move.product_uom._compute_quantity(move.product_uom_qty, product.uom_id)

            # 检查库存是否充足
            if product.tracking == 'none':
                available_qty = sum(q.quantity for q in product_lots.get(product.id, []))
                if float_compare(available_qty, move_qty, precision_rounding=rounding) < 0:
                    raise ValidationError(_("Insufficient inventory for %s!" % product.name))
            else:
                # 清理非法批次的move_line
                valid_lot_ids = product_lots[product.id].mapped('lot_id.id')
                move.move_line_ids.filtered(
                    lambda l: l.lot_id.id not in valid_lot_ids and l.state != 'done'
                ).unlink()

                self._handle_lot_assignment(move, product_lots.get(product.id, []), rounding)

            move._action_done()

    def _handle_lot_assignment(self, move, quants, rounding):
        """
        处理批次分配的辅助方法（强制使用原单批次）
        """
        move_lines = move.move_line_ids.filtered(lambda l: l.state not in ('done', 'cancel'))
        move_lines.unlink()  # 清除未完成的批次分配

        remaining_qty = move.product_uom_qty
        for quant in quants.sorted(lambda q: (q.lot_id.name, q.id)):  # 固定排序保证一致性
            if float_is_zero(remaining_qty, precision_rounding=rounding):
                break

            available = quant.quantity
            if float_is_zero(available, precision_rounding=rounding):
                continue

            assign_qty = min(available, remaining_qty)
            self.env['stock.move.line'].create({
                'move_id': move.id,
                'product_id': move.product_id.id,
                'product_uom_id': move.product_uom.id,
                'location_id': move.location_id.id,
                'location_dest_id': move.location_dest_id.id,
                'lot_id': quant.lot_id.id,
                'quantity': assign_qty,
                'date': datetime.datetime.now(),
                'company_id': move.company_id.id,
                'production_id': move.raw_material_production_id.id,
                'reference': move.raw_material_production_id.name or '',
                'lot_name': quant.lot_id.name,
            })
            remaining_qty -= assign_qty

        if not float_is_zero(remaining_qty, precision_rounding=rounding):
            raise ValidationError(_("Cannot assign all quantities for %s!" % move.product_id.name))

    def compute_station_order(self, all_production=False):
        if not all_production:
            p_ids = self
        else:
            p_ids = self.search([('state', 'not in', ('done', 'cancel'))])
        for p in p_ids:
            step = 1
            qty = sum(p.workorder_ids.mapped('qty_wip'))
            qty_pnl = sum(p.workorder_ids.mapped('qty_wip_pnl'))
            for station, g in groupby(p.workorder_ids.sorted(key=lambda x: x.step), key=lambda x: x.work_station_id):
                workorder_ids = self.env['mrp.workorder'].concat(*g).filtered(lambda x: not x.workcenter_id.s_not_compute_output)
                if not workorder_ids:
                    step += 1
                    continue
                if not workorder_ids.filtered(lambda x: x.state != 'done'):
                    step += 1
                    continue

                # workorder_id = workorder_ids[-1]
                # qty = workorder_id.qty_wip or workorder_id.qty_production
                # qty_pnl = workorder_id.qty_wip_pnl or workorder_id.s_qty_panel_remaining
                if workorder_ids.filtered(lambda x: x.s_station_order):

                    station_order_ids = workorder_ids.mapped('s_station_order')
                    if station_order_ids and len(station_order_ids) == 1:
                        station_order_ids.sudo().write({
                            'step': step,
                            'qty': qty,
                            'qty_pnl': qty_pnl,
                            'workorder_ids': [(4, w.id, False) for w in workorder_ids],
                        })
                        step += 1
                        continue
                    else:
                        if station_order_ids and len(station_order_ids) > 1:
                            workorder_ids.update({'s_station_order': False})
                            station_order_ids.sudo().unlink()

                self.sudo().env['s.mrp.work.station.order'].create({
                    'production_id': p.id,
                    'station_id': station.id,
                    'step': step,
                    'qty': qty,
                    'qty_pnl': qty_pnl,
                    'workorder_ids': [(4, w.id, False) for w in workorder_ids]
                })

                step += 1

    def _compute_production_qty(self):
        production_ids = self.search([('state', '=', 'done'), ('product_id.categ_id.code', '=', 'CHP')])
        for base_mo, g in groupby(production_ids.sorted(lambda x: x.base_mo), lambda x: x.base_mo):
            p_ids = self.concat(*g)
            if p_ids[0].location_dest_id.barcode != 'WH-PREPRODUCTION':
                for p in p_ids:
                    scrap_qty = sum(p.scrap_ids.mapped('scrap_qty'))
                    p.write({'s_receipt_qty': p.product_qty - scrap_qty, 's_pre_production_qty': 0})
            else:
                lot_ids = p_ids.mapped('finished_move_line_ids.lot_id')
                stock_move_line_ids = self.env['stock.move.line'].search([
                    ('location_id.barcode', '=', 'WH-PREPRODUCTION'),
                    ('location_dest_id.barcode', '=', 'WH-STOCK-FINISHED-PRODUCT'),
                    ('state', '=', 'done'),
                    ('lot_id', 'in', tuple(lot_ids.ids)),
                    ('move_id.picking_id.picking_type_id.barcode', '=', 'INT-PRODUCTIONIN')
                ])
                lot_qty_dict = {}
                for line in stock_move_line_ids:
                    if line.lot_id in lot_qty_dict:
                        lot_qty_dict[line.lot_id] += line.quantity
                    else:
                        lot_qty_dict[line.lot_id] = line.quantity

                for p in p_ids:
                    p_lot_qty_dict = {}
                    for p_l in p.finished_move_line_ids:
                        if p_l.lot_id in p_lot_qty_dict:
                            p_lot_qty_dict[p_l.lot_id] += p_l.quantity
                        else:
                            p_lot_qty_dict[p_l.lot_id] = p_l.quantity
                    for scrap in p.scrap_ids:
                        p_lot_qty_dict[scrap.lot_id] -= scrap.scrap_qty

                    for lot, qty in p_lot_qty_dict.items():
                        receipt_qty = 0
                        if lot in lot_qty_dict:
                            total_lot_qty = lot_qty_dict.get(lot, 0)
                            min_qty = min(qty, total_lot_qty)
                            receipt_qty += min_qty
                            lot_qty_dict[lot] -= min_qty
                            p_lot_qty_dict[lot] -= min_qty

                        p.write({'s_receipt_qty': receipt_qty, 's_pre_production_qty': sum(p_lot_qty_dict.values())})

    # 更新BOM
    def action_update_bom(self):
        raise UserError('This feature has been disabled!')

    # 去掉原生的限制，消耗物料和产成品不能相等
    def _onchange_product_id(self):
        return

    # ##################################  [ actions ]  ############################################################
    def action_wip_report_download(self):
        return {
            'type': 'ir.actions.act_url',
            'url': '/download/sb/production/wip/report/excel',
            'target': 'new',
        }

    def action_view_scrap_line(self):
        self.ensure_one()
        return {
            'name': _('Scrap Details'),
            'type': 'ir.actions.act_window',
            'view_mode': 'tree',
            'res_model': 's.workorder.scrap.record',
            'domain': [('id', 'in', self.s_scrap_line_ids.ids)],
            'views': [(self.env.ref('pcbmanufact.s_workorder_scrap_record_tree_view').id, 'tree')],
            'context': {'create': 0},
            'target': 'new',
        }

    def open_wip_mo(self):
        return {
            'name': 'WIP MO',
            'type': 'ir.actions.act_window',
            'view_type': 'form',
            'view_mode': 'tree',
            'res_model': 'mrp.workorder',
            'views': [(self.env.ref('pcbmanufact.sb_wip_workorder_pop_view_tree').id, 'tree')],
            'domain': [('production_id', '=', self.id)],
            'target': 'new',
        }

    def action_sb_child_cz(self):
        tree_id = self.env.ref('mrp.mrp_production_tree_view').id
        form_id = self.env.ref('mrp.mrp_production_form_view').id
        return {
            'name': 'Remake',
            'type': 'ir.actions.act_window',
            'view_type': 'form',
            'view_mode': 'form, tree',
            'res_model': 'mrp.production',
            'domain': [('id', 'in', self.sb_child_cz_ids.ids)],
            'views': [(tree_id, 'tree'), (form_id, 'form')],
            'context': {'create': 0},
        }

    def action_sb_child_cg(self):
        tree_id = self.env.ref('mrp.mrp_production_tree_view').id
        form_id = self.env.ref('mrp.mrp_production_form_view').id
        return {
            'name': 'Rework',
            'type': 'ir.actions.act_window',
            'view_type': 'form',
            'view_mode': 'form, tree',
            'res_model': 'mrp.production',
            'domain': [('id', 'in', self.sb_child_cg_ids.ids)],
            'views': [(tree_id, 'tree'), (form_id, 'form')],
            'context': {'create': 0},
        }

    def action_rework(self):
        qty_done = 0
        for i in self.sb_child_cg_ids:
            qty_done += i.product_qty
        for i in self.sb_child_cz_ids:
            qty_done += i.product_qty
        todo = self.qty_reject - qty_done
        if todo <= 0:
            raise ValidationError(_('The number of heavy work units to be installed cannot be less than or equal to 0'))

        new_wizard = self.env['workorder.redo.wizard'].create({
            'production_id': self.id,
            'mo_type': 'R',
            'qty_done': qty_done,
            'todo': self.qty_reject - qty_done
        })
        view_id = self.env.ref('pcbmanufact.view_workorder_redo_wizard').id

        return {
            'type': 'ir.actions.act_window',
            'name': _('Manufacturing Order Reworking-%s', self.name),
            'view_mode': 'form',
            'res_model': 'workorder.redo.wizard',
            'target': 'new',
            'res_id': new_wizard.id,
            'views': [[view_id, 'form']],
        }

    def action_remark(self):
        qty_done = 0
        for i in self.sb_child_cg_ids:
            qty_done += i.product_qty
        for i in self.sb_child_cz_ids:
            qty_done += i.product_qty
        todo = self.qty_reject - qty_done
        if todo <= 0:
            raise ValidationError(_('The quantity pending rework should not be less than or equal to zero.'))
        new_wizard = self.env['workorder.redo.wizard'].create({
            'production_id': self.id,
            'mo_type': 'M',
            'qty_done': qty_done,
            'todo': self.qty_reject - qty_done
        })
        view_id = self.env.ref('pcbmanufact.view_workorder_redo_wizard').id

        return {
            'type': 'ir.actions.act_window',
            'name': _('Manufacturing Order Reprocessing-%s', self.name),
            'view_mode': 'form',
            'res_model': 'workorder.redo.wizard',
            'target': 'new',
            'res_id': new_wizard.id,
            'views': [[view_id, 'form']],
        }
